package com.pic.mgt.controller;

import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;

import com.pic.mgt.IgxStatus;
import com.pic.mgt.dao.AbstractBusinessMgtInfoBean;
import com.pic.mgt.dao.IBusinessMgtDao;
import com.pic.mgt.log.IgxLog;
import com.pic.mgt.monitor.UnitMonitorListener;
import com.pic.mgt.monitor.backup.UnitBackup;
import com.pic.mgt.unit.Business;
import com.pic.mgt.unit.Redoable;
import com.pic.mgt.unit.Unit;
import com.pic.mgt.unit.operation.BusinessUnitOperation;
import com.pic.mgt.unit.operation.UnitOperationException;

public abstract class BusinessUnitController extends UnitController
							implements UnitMonitorListener {
	protected IBusinessMgtDao businessMgtDao;
	protected BusinessUnitOperation businessUnitOperation;
	
	private boolean isDoBizConfirmationFileRedundantCheck;
	private boolean isIgnoreDuplicateBusiness;
	
	private final long redundantCheckRetryWaitingLimit = 60000;
	private Map<Business, Long> redundantCheckRetryMap;
	
	public BusinessUnitController() {
		super();
		
		this.redundantCheckRetryMap = new HashMap<Business, Long>();
	}
	
	@Override
	public void doUnitAction(Unit unit) {
		Business business = (Business)unit;	
		
		/*
		 *  先確認是否為有效的端未(由子類別確認)。
		 *  若為無效端末則不繼續往下做。
		 *  此判斷動作,只有集信有作用,配信不會去判斷。
		 */
		String ineffectiveTerminal = checkIsEffectiveTerminal(business);
		if (null != ineffectiveTerminal) {
			/*
			 * 集信上傳的檔案，如果 terminal id 為無效的，仍然會處理，但要輸出 Warn.
			 */
			
			// ********************** Log **********************
			if (IgxLog.messageLog.isWarnMode()) {
				IgxLog.messageLog.warn(this,
						"The terminal id '{}' of business '{}' is ineffective.",
						ineffectiveTerminal, unit.getUnitName());
			}
			// ********************** Log **********************
		}
		
		
		// 做資料庫資料的新增 (table:haishin_business_mgt 或  syushin_business_mgt)。
		try {
			// throws DuplicateKeyException
			boolean isInsertSuccess = 
						insertDBToBusinessMgtTable(business);
			if (!isInsertSuccess) {
				return;
			}
		} catch (DuplicateKeyException e) {
			/*
			 *  代表已經有重覆的業務(先前已經做過)，所以無法新增至資料庫中。
			 *  處理方式：取出上次業務執行的狀態:
			 *  (1) 判斷上次的狀態是否為不允許重覆做的業務
			 *  (2)若是允許重覆做，則交由子類別處理後並回傳得到被 Redoable wapper interface 
			 *     包起來的業務。
			 *  
			 */	
			try {
				// 從資料庫中取出該業務基本資訊
				AbstractBusinessMgtInfoBean businessMgtInfoBean = 
											getBusinessMgtInfoBeanInDB(business.getUnitName());
				// 若資料庫找不到詃資料,則表示有問題!! (因為重覆卻又找不到該筆資料?)
				if (null == businessMgtInfoBean) {
					super.unitBackup.removeAndBackupErrorUnit(business.getConfirmationFile());
					
					// ********************** Log **********************
					IgxLog.messageLog.error(this,
									"SUDA-273 A duplicated business '{}' occured. But can't find previous business info in database. (Business has been moved to error area.)",
									business.getUnitName());
					// ********************** Log **********************
					
					return;
				}
				
				
				
				// 取得該資料庫中該業務的狀態
				// throws DataAccessException
				String lastBusinessStatus = businessMgtInfoBean.getStatus();
				
				
				// 判斷上次的狀態是否允許重覆做的業務
				boolean isBusinessCanRedo =
							checkIsBusinessCanRedoByLastStatus(lastBusinessStatus);
				if (!isBusinessCanRedo) { // 不允許重覆做業務
					/*
					 * 如果有開啟"多餘確認檔檢核"機制，那麼便要先去檢查確認檔是否存在，
					 * 如果不存在則表示是多餘的確認檔，不用處理只須拋Warnning。
					 * 會發生此問題，主要是在於PP集信上傳檔案，因用3G/GPRS網路連線不穩，容易多傳一個
					 * 確認檔上來或者是多產生一個close事件，這二種情況都會造成確認檔多被偵測一次，導
					 * 致可能會在前端parse business時做了二次，來到這裡後第一個business已被處理(已
					 * 新增至DB中了)，再來第二個相同的business的話就會產生資料重覆新增的Exception了，
					 * 所以第二次的business事實上可以被忽略。
					 * 
					 */
					if (this.isDoBizConfirmationFileRedundantCheck) {
						if (lastBusinessStatus.equals(IgxStatus.BUSINESS_COMPLETED) ||
							lastBusinessStatus.equals(IgxStatus.BUSINESS_BATCH_TMP)) {
							/*
							 * 必須確定業務的狀態是＂已完成＂，才能夠準確去判斷。因為若是未完成表示還在執行
							 * 中，執行中的話目標檔和確認檔本來就還不會被移除。
							 */
							
							if (!business.getConfirmationFile().exists()) {
								this.redundantCheckRetryMap.remove(business);
								
								// ********************** Log **********************
								IgxLog.messageLog.warn(this,
										"An nonexistent redundant confirmation file '{}' has been detected and ignored.",
										business.getConfirmationFile().getAbsolutePath());
								// ********************** Log **********************

								return;
							}
						}
						else {
							if (!lastBusinessStatus.equals(IgxStatus.BUSINESS_FAILED)) {
								//若業務狀態還在執行中，先不作處理並放回Queue去。
								
								if (!this.redundantCheckRetryMap.containsKey(business)) {
									super.unitIncoming(Collections.singletonList((Unit)business));
									this.redundantCheckRetryMap.put(business, System.currentTimeMillis());
									return;
								}
								else {
									long entryTime = this.redundantCheckRetryMap.get(business);
									if ((System.currentTimeMillis()-entryTime) < this.redundantCheckRetryWaitingLimit) {
										super.unitIncoming(Collections.singletonList((Unit)business));
										return;
									}
								}
							}
						}
					}
					
					// ------- 程式走到此一區塊，表示業務為重覆  ----------
					
					this.redundantCheckRetryMap.remove(business);
					
					/*
					 *  業務重覆的情況下，要去判斷是否有開啟 "忽略業務重覆" 的功能，若有就必須要忽略，不要輸出 ERROR。
					 *  PS: 若檔案大小與資料庫中target file 檔案大小不一樣,則還是不能忽略(縱使開起了"忽略業務重覆"功能),還是必須拋 ERROR。
					 */
					if (this.isIgnoreDuplicateBusiness &&
						business.getTargetFile().length()==businessMgtInfoBean.getTargetFilesize()) {
						try {
							this.businessUnitOperation.removeUnit(business);
							
							// ********************** Log **********************
							IgxLog.messageLog.warn(this,
											"SUDA-205 '{}' is a duplicated business and has been removed.",
											business.getUnitName());
							// ********************** Log **********************
						} catch (UnitOperationException e1) {
							// ********************** Log **********************
							IgxLog.messageLog.error(this,
											"SUDA-200 Removing duplicated business '{}' related files failed.",
											business.getUnitName());
							// ********************** Log **********************
						}
					}
					else {
						super.unitBackup.removeAndBackupErrorUnit(business.getConfirmationFile());
						
						// ********************** Log **********************
						IgxLog.messageLog.error(this,
										"SUDA-205 '{}' is a duplicated business and has been moved to error area.",
										business.getUnitName());
						// ********************** Log **********************
					}
					
					return;
				}
				
				// 以下為允許重覆做的業務處理 -----------
				
				// update 已經存在於資料庫中,該筆業務的狀態和creating time
				// throws DataAccessException
				this.businessMgtDao.updateStatusAndCreatingTimeByUnitName(
						IgxStatus.BUSINESS_LOADED, new Date(), business.getUnitName());
				
				// 取得該業務的  Redoable wapper
				business = (Business)wrapToAnRedoableBusiness(business);

			} catch (DataAccessException e2) {
				// ********************** Log **********************
				IgxLog.messageDetailsLog.error(this, e2.getMessage());
				// ********************** Log **********************
				
				// ********************** Log **********************
				IgxLog.messageLog.error(this,
						"SUDA-206 Loading business '{}' info from database failed. (table: {}).",
						business.getUnitName(), this.businessMgtDao.getTableName());
				// ********************** Log **********************
				
				return;
			}
		}
		
		// ********************** Log **********************
		if (IgxLog.messageLog.isDebugMode()) {
			IgxLog.messageLog.debug(this, "Backuping business '{}'.",
					business.getUnitName());
		}
		// ********************** Log **********************
		
		// 做業務備份(目標檔案和確認檔案)
		boolean completeBackup =
			this.unitBackup.backupUnit(business);
		if (!completeBackup) {
			return;
		}
		
		// ********************** Log **********************
		if (IgxLog.messageLog.isDebugMode()) {
			IgxLog.messageLog.debug(this, "Backuping business '{}' completed.",
					business.getUnitName());
		}
		// ********************** Log **********************
		
		doBusinessAction(business);
	}
	
	private boolean checkIsBusinessCanRedoByLastStatus(String lastStatus) {
		if (lastStatus.equals(IgxStatus.BUSINESS_REDO)) {
			return true;
		}
		else {
			return false;
		}
	}
	
	/**
	 * 做資料庫的新增動作。若新增發現已有相同的業務存在於資料表中，則不會再做新增，
	 * 並會在Business內填入上次該業務處理的狀態(lastStatu)。
	 * @param business
	 * @return
	 */
	private boolean insertDBToBusinessMgtTable(Business business) throws DuplicateKeyException {
		// 資料庫新增 (table:haishin_business_mgt 或  syushin_business_mgt)。
		try {
			Map<String, Object> inserValuesMap = getDbBusinessMgtInsertBasicValuesMap(business);
			// throws DataAccessException(DuplicateKeyException)
			int insertRows =
				this.businessMgtDao.insertBasic(inserValuesMap);
			
			// ********************** Log **********************
			if (IgxLog.messageLog.isTraceMode()) {
				IgxLog.messageLog.trace(this,
						"Insert business record '{}' to database (table: {}) completed.",
						business.getUnitName(), this.businessMgtDao.getTableName());
			}
			// ********************** Log **********************
		} catch(DuplicateKeyException e1) {
			throw e1;
		} catch (DataAccessException e2) {
			// ********************** Log **********************
			IgxLog.messageDetailsLog.error(this, e2.getMessage());
			// ********************** Log **********************
			
			// ********************** Log **********************
			IgxLog.messageLog.error(this,
					"SUDA-207 Inert business record '{}' to database (table: {}) failed.",
					business.getUnitName(), this.businessMgtDao.getTableName());
			// ********************** Log **********************
			
			return false;
		}
		
		return true;
	}
	
	/**
	 * 子類別判斷是否為有效的端末。
	 * @param unit
	 * @return 回傳為字串。若為 null 表示為有效的端末；若不是 null，則回傳該
	 * 		       無效的端末 ID。
	 */
	protected abstract String checkIsEffectiveTerminal(Business business);
	
	/**
	 * 將Business 包成為一個有 Redoable 介面行為的 Business
	 * @param lastUnitStatus
	 * @param unit
	 * @return 回傳的是實作 Redoable 介面,俱備有 redoable 方法的 Unit.
	 */
	protected abstract Redoable wrapToAnRedoableBusiness(Business business);
	
	/**
	 * 當 Business 到達時, 會先進行資料庫資料的新增。必須實作此方法提供新增的欄位值。
	 * @param business
	 * @return
	 */
	protected abstract Map<String, Object> getDbBusinessMgtInsertBasicValuesMap(Business business);
	
	/**
	 * 有 Business 到達時,方法會被觸發。(此方法是在 getDbBusinessMgtInsertBasicValuesMap()之後被呼叫)
	 * @param business 到達的 Business
	 */
	public abstract void doBusinessAction(Business business);
	
	/**
	 * 從資料庫(*_business_mgt)中,依照給定的 Unit Name,撈出資料(成為 AbstractBusinessMgtInfoBean)
	 * @param unitName
	 * @return
	 * @throws DataAccessException
	 */
	public abstract AbstractBusinessMgtInfoBean getBusinessMgtInfoBeanInDB(String unitName) throws DataAccessException;
	

	// --- getter and setter ---

	public void setUnitBackup(UnitBackup unitBackup) {
		super.unitBackup = unitBackup;
	}

	public void setBusinessMgtDao(IBusinessMgtDao businessMgtDao) {
		this.businessMgtDao = businessMgtDao;
	}

	public void setDoBizConfirmationFileRedundantCheck(
			boolean isDoBizConfirmationFileRedundantCheck) {
		this.isDoBizConfirmationFileRedundantCheck = isDoBizConfirmationFileRedundantCheck;
	}

	public void setIgnoreDuplicateBusiness(boolean isIgnoreDuplicateBusiness) {
		this.isIgnoreDuplicateBusiness = isIgnoreDuplicateBusiness;
	}

	public void setBusinessUnitOperation(BusinessUnitOperation businessUnitOperation) {
		this.businessUnitOperation = businessUnitOperation;
	}
}
